<!DOCTYPE html>
<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
    <meta charset="UTF-8">
    <title>MVVM 双项绑定</title>
    <style>
        #app {
            text-align: center;
            margin-top: 100px;
            color: #888;
        }

        h1 {
            color: #aaa;
        }

        input {
            padding: 0 10px;
            width: 600px;
            line-height: 2.5;
            border: 1px solid #ccc;
            border-radius: 2px;
        }

        .bind {
            color: #766;
        }

        strong {
            color: #05BC00;
        }

        button {
            padding: 5px 10px;
            border: 1px solid #777777;
            border-radius: 5px;
            background: #ffffff;
            color: #777777;
            cursor: pointer;

        }
    </style>
</head>
<body>
<div id="app">
    <h1>Hi，MVVM</h1>
    <input v-model="name" placeholder="请输入内容" type="text">
    <h1 class="bind">{{name}} 's age is <strong>{{age}}</strong></h1>
    <button v-on:click="sayHi">点击欢迎您</button>
</div>
<script>
    function observe(data) {
        //如果不是一个对象，直接终止程序
        if (!data || typeof data !== 'object') {
            return false;
        }
        for (let key in data) {
            let val = data[key];
            let subject = new Subject();
            Object.defineProperty(data, key, {
                enumerable: true,
                configurable: true,
                get: function () {
                    if (currentObserver) {
                        currentObserver.subscribeTo(subject)
                    }
                    return val
                },
                set: function (newVal) {
                    val = newVal;
                    subject.notify()
                }
            });
            if (typeof val === 'object') {
                observe(val)
            }
        }
    }

    let id = 0;
    let currentObserver = null;

    /**
     * 订阅者对象
     */
    class Subject {
        constructor() {
            this.id = id++;
            this.observers = []
        }

        addObserver(observer) {
            this.observers.push(observer)
        }

        removeObserver(observer) {
            let index = this.observers.indexOf(observer)
            if (index > -1) {
                this.observers.splice(index, 1)
            }
        }

        notify() {
            this.observers.forEach(observer => {
                observer.update()
            })
        }
    }

    /**
     * 观察者对象
     */
    class Observer {
        constructor(vm, key, cb) {
            this.subjects = {};
            this.vm = vm;
            this.key = key;
            this.cb = cb;
            this.value = this.getValue()
        }

        //如果新旧数据不相同，就直接调用cb方法
        update() {
            let oldVal = this.value;
            let value = this.getValue();
            if (value !== oldVal) {
                this.value = value;
                this.cb.bind(this.vm)(value, oldVal)
            }
        }

        //添加观察者
        subscribeTo(subject) {
            if (!this.subjects[subject.id]) {       //如果当前换擦着中不存在这个当前id的一个对象，那么吧这个对象添加为观察者
                subject.addObserver(this);
                this.subjects[subject.id] = subject     //放在观察者对象中，根据自增id来区分
            }
        }

        getValue() {
            currentObserver = this;
            let value = this.vm.$data[this.key];    //获取vm实例兑现中的data数据
            currentObserver = null;
            return value
        }
    }

    /**
     * 编译对象
     */
    class Compile {
        constructor(vm) {
            this.vm = vm; //vm对象
            this.node = vm.$el; //获取挂载的元素dom
            this.compile();//执行核心功能
        }

        compile() {
            this.traverse(this.node);//传入的参数是挂载元素dom
        }

        traverse(node) {
            if (node.nodeType === 1) {      //节点类型1：element元素
                this.compileNode(node);     //触发节点事件 双向绑定和事件触发
                node.childNodes.forEach(childNode => {
                    this.traverse(childNode);       // 递归调用，如果是有子节点，重新递归
                })
            } else if (node.nodeType === 3) {       // 节点类型3： 文本元素
                this.compileText(node);     // 处理文本元素的编译
            }
        }

        // 文本编译入口
        compileText(node) {
            let reg = /{{(.+?)}}/g;
            let match;
            while (match = reg.exec(node.nodeValue)) {      //获取到文本内容
                let raw = match[0]
                let key = match[1].trim()
                node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key]);
                new Observer(this.vm, key, function (val, oldVal) {     // 订阅者核心方法
                    node.nodeValue = node.nodeValue.replace(oldVal, val)
                })
            }
        }

        // 节点编译入口
        compileNode(node) {
            let attrs = [...node.attributes];//获取标签属性
            attrs.forEach(attr => {
                if (this.isModelDirective(attr.name)) { //截取是绑定数据的情况
                    this.bindModel(node, attr); //绑定数据
                } else if (this.isEventDirective(attr.name)) { //截取是绑定事件的情况
                    this.bindEventHander(node, attr); //触发事件
                }
            })
        }

        /**
         * 双向绑定数据
         * @param node  标签节点
         * @param attr  标签节点的属性名
         */
        bindModel(node, attr) {
            let key = attr.value;// 获取到传递过来的属性的key的值
            node.value = this.vm.$data[key]; //给节点绑定值，对应的值就是vm实例里面data对应key的值
            new Observer(this.vm, key, function (newVal) {
                node.value = newVal
            });
            node.oninput = (e) => { //监听节点的input事件
                this.vm.$data[key] = e.target.value //过去输入框中输入的value值，把这个值放入到vm的data实例中去
            }
        }

        /**
         *
         * @param node
         * @param attr
         */
        bindEventHander(node, attr) {
            let eventType = attr.name.substr(5); //获取节点属性,从第五个下标开始截取后面的字符串作为：key(事件类型)
            let methodName = attr.value; //获取节点的属性的value
            node.addEventListener(eventType, this.vm.$methods[methodName]);//通过事件类型，来触发事件，事件就是vm实例中方法
        }

        //赛选出传入的node属性是v-model的情况
        isModelDirective(attrName) {
            return attrName === 'v-model'
        }

        //赛选出传入的node属性是 v-on的情况
        isEventDirective(attrName) {
            return attrName.indexOf('v-on') === 0
        }
    }

    class mvvm {
        constructor(opts) {     //这里面的函数是实例化的时候执行的
            this.init(opts);
            observe(this.$data);
            new Compile(this);      //变异当前对象
        }

        init(opts) {
            if (opts.el.nodeType === 1) {
                this.$el = opts.el
            } else {
                this.$el = document.querySelector(opts.el)
            }

            this.$data = opts.data || {};
            this.$methods = opts.methods || {};
            //把$data 中的数据直接代理到当前 vm 对象
            for (let key in this.$data) {
                Object.defineProperty(this, key, {
                    enumerable: true,
                    configurable: true,
                    get: () => {
                        return this.$data[key]
                    },
                    set: newVal => {
                        this.$data[key] = newVal
                    }
                })
            }
            //让 this.$methods 里面的函数中的 this，都指向当前的 this，也就是 vm对象实例
            for (let key in this.$methods) {
                this.$methods[key] = this.$methods[key].bind(this);
            }
        }
    }


    /**
     * 实例化MVVM对象， 主入口
     * @type {mvvm}
     */
    let vm = new mvvm({
        el: '#app',
        data: {
            name: 'YanLe',
            age: 3
        },
        methods: {
            sayHi: function () {
                alert(`hi ${this.name}`)
            }
        }
    });

    let clock = setInterval(function () {
        vm.age++;  //等同于 vm.$data.age

        if (vm.age === 10) clearInterval(clock)
    }, 1000)
</script>
</body>
</html>